/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package ru.niir.goodfil.ui;

import javassist.NotFoundException;
import org.apache.commons.lang.StringUtils;
import org.apache.log4j.Logger;
import org.apache.poi.hssf.usermodel.HSSFWorkbook;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.Workbook;
import org.richfaces.event.UploadEvent;
import org.richfaces.model.UploadItem;
import ru.niir.goodfil.db.ClientGroup;
import ru.niir.goodfil.db.Good;
import ru.niir.goodfil.db.GoodType;
import ru.niir.goodfil.exceptions.CreateFailureException;
import ru.niir.goodfil.exceptions.GoodfilSecurityException;
import ru.niir.goodfil.exceptions.ValidationException;
import ru.niir.goodfil.utils.FloatPrice;
import ru.niir.goodfil.utils.PricelistItem;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.util.*;

import static org.apache.commons.lang.StringUtils.isEmpty;
import static ru.niir.goodfil.utils.Utils.*;

/*
    29.04.10
    - Добавил настройку, влияющую на то, будут ли при импорте прейскуранта добавляться товары в БД
    - Исправил ошибку с пропуском последней колонки
    - Сделал округление цены
 */

public class ImportPricelistBean extends PageBean
{
    private final static Logger logger = Logger.getLogger(ImportPricelistBean.class);
    public ImportPricelistBean()
    {
        if (!getSecurityBean().getIsRoleAdministrator())
            throw new GoodfilSecurityException("Только администраторы имеют доступ к функции \"Импорт прейскуранта из Excel\"");
    }

    /*
        НАСТРОЙКИ, СВЯЗАННЫЕ С ЗАГРУЗКОЙ ПРЕЙСКУРАНТА ИЗ EXCEL
     */

    //  Каталог, куда сохраняются при загрузке файлы с прейскурантами
    private String getSHARED_CATALOG() { return getValueOfDefault("import.pricelist.shared.catalog", "/usr/home/goodwill"); }

    //  Максимальное количество выводимых ошибок.
    //  Другими словами: "выводить только первые N ошибок".
    private int getMAX_ERRORS() { return getValueOfDefault("import.pricelist.max.errors", 20); }

    //  Номер колонки с кодом
    private int getCODE_POS() { return getValueOfDefault("import.pricelist.code.pos", 0); }

    //  Номер колонки с группой товара
    private int getGROUP_POS() { return getValueOfDefault("import.pricelist.group.pos", 1); }

    //  Номер колонки с наименованием товара
    private int getNAME_POS() { return getValueOfDefault("import.pricelist.name.pos", 2); }

    //  Номер колонки с M&H
    private int getMH_POS() { return getValueOfDefault("import.pricelist.mh.pos", 3); }

    //  Номер колонки с о.е.
    private int getOE_POS() { return getValueOfDefault("import.pricelist.oe.pos", 4); }

    //  Номер колонки с применимостью
    private int getPR_POS() { return getValueOfDefault("import.pricelist.pr.pos", 5); }

    //  Номер колонки "Групповая тара"
    private int getPC_POS() { return getValueOfDefault("import.pricelist.pc.pos", 6); }

    //  Номер колонки с минимальной ценой ("Торговый дом")
    private int getMINPRICE_POS() { return getValueOfDefault("import.pricelist.minprice.pos", 7); }

    //  Номер колонки, начиная с который идут колонки цен для клиентских групп
    private int getGR_POS() { return getValueOfDefault("import.pricelist.gr.pos", 8); }

    /**
     * Функция позволяет получить строковое значение опции, если она уже создана и создать опцию в противном случае.
     * @param settingName наименование опции
     * @param defaultValue значение по-умолчанию, которое будет установлено, если опция отсутствует в БД
     * @return строковое значение опции (старой или только что созданной)
     */
    private String getValueOfDefault(String settingName, String defaultValue)
    {
        gf().createIfNotExists(settingName, defaultValue);
        return gf().get(settingName);
    }

    /**
     * Функция позволяет получить числовое значение опции, если она уже создана и создать опцию в противном случае.
     * @param settingName наименование опции
     * @param defaultValue значение по-умолчанию, которое будет установлено, если опция отсутствует в БД
     * @return строковое числовое опции (старой или только что созданной)
     */
    private int getValueOfDefault(String settingName, int defaultValue)
    {
        gf().createIfNotExists(settingName, String.valueOf(defaultValue));
        return gf().getInt(settingName);
    }

    /**
     * Типы сообщений при загрузке прейскуранта из Excel.
     * После загрузки прейскуранта из Excel пользователю выводятся сообщения, позволяющие судить о том,
     * насколько успешно прошла процедура загрузки прейскуранта. В этом перечне сообщений могут встречаться
     * сообщения об ошибках и информационные сообщения.
     *
     * @see ru.niir.goodfil.ui.ImportPricelistBean.ImportMessage Класс для хранения сообщений о процессе загрузки прейскуранта.
     */
    public enum ImportMessageType
    {
        /**
         * Информационные сообщения
         */
        INFO,

        /**
         * Сообщения об ошибках
         */
        ERROR
    }

    /**
     * Класс для хранения сообщений о процессе загрузки прейскуранта.
     *
     * @see ru.niir.goodfil.ui.ImportPricelistBean.ImportMessageType Типы сообщений.
     */
    public static class ImportMessage
    {
        /**
         * Тип сообщения
         */
        private ImportMessageType type;

        /**
         * Текст сообщения
         */
        private String message;

        /**
         * Конструктор
         */
        public ImportMessage()
        {
        }

        /**
         * Конструктор
         * @param type тип сообщения
         * @param message текст сообщения
         */
        public ImportMessage(ImportMessageType type, String message)
        {
            _assertNotBlank(message);

            this.type = type;
            this.message = message;
        }

        /**
         * Возвращает true, если данной сообщение является информационным
         */
        public boolean getIsInfo()
        {
            return type == ImportMessageType.INFO;
        }

        /**
         * Возвращает true, если данное сообщение является сообщением об ошибке
         */
        public boolean getIsError()
        {
            return type == ImportMessageType.ERROR;
        }

        /**
         * Тип сообщения
         */
        public ImportMessageType getType()
        {
            return type;
        }

        /**
         * Тип сообщения
         */
        public void setType(ImportMessageType type)
        {
            this.type = type;
        }

        /**
         * Текст сообщения
         */
        public String getMessage()
        {
            return message;
        }

        /**
         * Текст сообщения
         */
        public void setMessage(String message)
        {
            this.message = message;
        }
    }

    /**
     * Список сообщений, которые выводятся пользователю по результатам загрузки прейскуранта
     */
    private final List<ImportMessage> messages = new ArrayList<ImportMessage>();

    /**
     * Список сообщений, которые выводятся пользователю по результатам загрузки прейскуранта
     */
    public List<ImportMessage> getMessages()
    {
        return messages;
    }

    /**
     * Обработчик закачки файла.
     * Закачка файла реализована через RichFaces file upload component.
     */
    public void listener(UploadEvent event) throws Exception
    {
        try
        {
            messages.clear();

            //  Получаем закаченый файл
            UploadItem item = event.getUploadItem();
            String filename = item.getFileName();
            String ext = filename.substring(filename.lastIndexOf('.') + 1);
            File file = item.getFile();

            //  Сохраняем закаченый файл
            UUID uid = saveFile(file, ext);
            if (uid == null)
            {
                messages.add(new ImportMessage(ImportMessageType.ERROR, "Невозможно скопировать файл в системную директорию. Обратитесь к администратору."));
                return;
            }

            //  Если файл удалось закачать - разбираем его
            importExcelFile(getSHARED_CATALOG() + File.separator + uid + "." + ext);
        }
        catch (Exception e)
        {
            e.printStackTrace();
        }
    }

    /**
     * Перечень групп товаров (с ленивой загрузкой)
     */
    private List<GoodType> goodTypes = null;

    /**
     * Перечень групп товаров (с ленивой загрузкой)
     */
    public List<GoodType> getGoodTypes()
    {
        return gf().getGoodTypes();
    }

    /**
     * Перечень клиентских групп с ленивой загрузкой
     */
    private List<ClientGroup> clientGroups = null;

    /**
     * Перечень клиентских групп с ленивой загрузкой
     */
    public List<ClientGroup> getClientGroups()
    {
        if (clientGroups == null)
        {
            clientGroups = gf().getClientGroups();
        }
        return clientGroups;
    }

    /**
     * Найти клиентскую группу по имени. Регистр букв не учитывается.
     * @param name наименование клиентской группы
     */
    private ClientGroup getClientGroupByName(String name)
    {
        name = s(name).toLowerCase();
        for (ClientGroup clientGroup : getClientGroups())
        {
            String clientGroupName = s(clientGroup.getName()).toLowerCase();
            if (clientGroupName.equals(name))
                return clientGroup;
        }
        return null;
    }

    private Workbook wb;
    private Sheet ws;

    public String getColumnName(int columnIndex)
    {
        _assertNotNull(wb, "К этому моменту Workbook уже должен быть проинициализирован");
        _assertNotNull(ws, "К этому моменту Worksheet уже должен быть проинициализирован");

        return ws.getRow(0).getCell(columnIndex).getStringCellValue();
    }

    /**
     * Разбор файла
     * @param filename имя файла
     */
    private void importExcelFile(String filename)
    {
        //  Передернуть кэш клиентских групп и групп товаров перед каждой новой загрузкой
        goodTypes = null;
        clientGroups = null;

        try
        {
            try
            {
                wb = new HSSFWorkbook(new FileInputStream(filename));
            }
            catch (IOException e)
            {
                e.printStackTrace();
                messages.add(new ImportMessage(ImportMessageType.ERROR, "Невозможно прочитать файл из системной директории"));
                return;
            }

            _assert(wb != null);

            if (wb == null)
            {
                messages.add(new ImportMessage(ImportMessageType.ERROR, "Невозможно открыть файл из системной директории"));
                return;
            }

            ws = wb.getSheetAt(0);
            if (ws == null)
            {
                messages.add(new ImportMessage(ImportMessageType.ERROR, "В файле не присутствует ни одного листа"));
                return;
            }

            Row firstRow = ws.getRow(0);
            if (firstRow == null)
            {
                messages.add(new ImportMessage(ImportMessageType.ERROR, "В файле отсутствует шапка"));
                return;
            }

            if (firstRow.getLastCellNum() < getGR_POS())
            {
                messages.add(new ImportMessage(ImportMessageType.ERROR, "В файле не хватает колонок"));
                return;
            }

            //  Карта соответствий колонок с ценами и клиентских групп
            Map<Integer /*cellNumber*/, Long /*clientGroupId*/> columns = new HashMap<Integer, Long>();
            try
            {
                for (int i = getGR_POS(); i < firstRow.getLastCellNum(); i++)
                {
                    String clientGroupName = firstRow.getCell(i).getStringCellValue();
                    if (StringUtils.isBlank(clientGroupName))
                    {
                        messages.add(new ImportMessage(ImportMessageType.ERROR, "Все колонки с ценами должны содержать в шапке наименование существующей клиентской группы. Проверьте колонку номер " + i));
                        return;
                    }

                    ClientGroup clientGroup = getClientGroupByName(clientGroupName);
                    if (clientGroup == null)
                    {
                        messages.add(new ImportMessage(ImportMessageType.ERROR, "Отсутствует клиентская группа с наименованием \"" + clientGroupName + "\""));
                        return;
                    }

                    columns.put(i, clientGroup.getId());
                }
            }
            catch (Exception e)
            {
                e.printStackTrace();
                messages.add(new ImportMessage(ImportMessageType.ERROR, "Шапка файла заполнена неверно"));
                return;
            }

            List<ExcelPricelistItem> items = new ArrayList<ExcelPricelistItem>();

            for(int y = 1; y <= ws.getLastRowNum(); y++)
            {
                Row row = ws.getRow(y);
                ExcelPricelistItem item = new ExcelPricelistItem(y);
                for (int x = 0; x < row.getLastCellNum(); x++)
                {
                    Cell cell = row.getCell(x);
                    if (cell == null) continue;

                    String value = null;
                    switch (cell.getCellType())
                    {
                        case Cell.CELL_TYPE_STRING:
                            value = cell.getStringCellValue();
                            break;
                        case Cell.CELL_TYPE_BLANK:
                            value = "";
                            break;
                        case Cell.CELL_TYPE_FORMULA:
                            value = cell.getStringCellValue();
                            break;
                        case Cell.CELL_TYPE_NUMERIC:
                            value = String.valueOf(cell.getNumericCellValue());
                            break;
                        default:
                            messages.add(new ImportMessage(ImportMessageType.ERROR, "Неверное значение в ячейке: строка " + y + ", столбец " + x));
                            return;
                    }


                    try
                    {
                        item.set(x, value, columns);
                    }
                    catch (NumberFormatException nfe)
                    {
                        if (!StringUtils.isBlank(item.getCode()))
                        {
                            if (messages.size() >= getMAX_ERRORS())
                            {
                                messages.add(new ImportMessage(ImportMessageType.ERROR, ".. и другие ошибки"));
                                return;
                            }
                            messages.add(new ImportMessage(ImportMessageType.ERROR, "Ячейка (" + String.valueOf(x+1) + ", " + String.valueOf(y+1) + ") имеет неверное значение: \"" + s(value) + "\". Значение должно быть числом"));
                        }
                    }
                    catch (NullPointerException nfe)
                    {
                        if (!StringUtils.isBlank(item.getCode()))
                        {
                            if (messages.size() >= getMAX_ERRORS())
                            {
                                messages.add(new ImportMessage(ImportMessageType.ERROR, ".. и другие ошибки"));
                                return;
                            }
                            messages.add(new ImportMessage(ImportMessageType.ERROR, "Ячейка (" + String.valueOf(x+1) + ", " + String.valueOf(y+1) + ") имеет неверное значение: \"" + s(value) + "\". Значение должно быть числом"));
                        }
                    }
                }

                if (!StringUtils.isBlank(item.getCode()))
                {
                    items.add(item);
                }
            }

            for (ImportMessage message : messages) if (message.getType() == ImportMessageType.ERROR) return;

            for (ExcelPricelistItem item : items) messages.addAll(item.validate());

            for (ImportMessage message : messages) if (message.getType() == ImportMessageType.ERROR) return;


            gf().createIfNotExists("import.pricelist.create.goods", String.valueOf(1), "Создавать новые товары при загрузке прейскуранта из Excel");
            boolean createGoods = (gf().getInt("import.pricelist.create.goods") != 0);

            if (!createGoods)
            {
                for (ExcelPricelistItem item : items)
                {
                    Good good = gf().getGoodByCode(item.getCode());
                    if (good == null)
                    {
                        messages.add(new ImportMessage(ImportMessageType.ERROR, "В БД не найдено товара с кодом: " + item.getCode()));
                    }
                }
            }

            messages.add(new ImportMessage(ImportMessageType.INFO, "Загружено " + items.size() + " строк (проверьте!)."));

            messages.add(new ImportMessage(ImportMessageType.INFO, "Копирование в БД..."));

            int okCount = 0;

            for (ExcelPricelistItem item : items)
            {
                logger.debug("excel pricelist item: " + item);
                Good good = gf().getGoodByCode(item.getCode());
                if (good == null)
                {
                    if (createGoods) good = new Good();
                    else continue;
                }

                good.setCode(item.getCode());
                good.setName(item.getName());
                good.setGoodTypeId(item.getGoodTypeId());
                good.setPackageCount(item.getPackageCount());
                good.setPr(item.getPr());
                good.setOe(item.getOe());
                good.setMh(item.getMh());
                good.setMinPrice(item.getMinPrice());

                try
                {
                    logger.debug("good: " + good);
                    if (good.getId() == -1) gf().saveGood(good);
                    else                    gf().updateGood(good);

                    PricelistItem pi = new PricelistItem(good);
                    for (long clientGroupId : item.getPrices().keySet())
                    {
                        float price = item.getPrices().get(clientGroupId);

                        //  Двойное присваивание нужно, чтобы цены ушли в БД
                        //  Без этого работать не будет
                        pi.put(clientGroupId, new FloatPrice(0));
                        pi.get(clientGroupId).setValue(price);
                    }

                    gf().updatePricelistItem(pi);
                    okCount++;
                }
                catch (ValidationException e)
                {
                    logger.error(e);
                    e.printStackTrace();
                    messages.add(new ImportMessage(ImportMessageType.ERROR, "Невозможно сохранить информацию о товаре: " + e.getResult().toHumanString()));
                }
                catch (CreateFailureException e)
                {
                    logger.error(e);
                    e.printStackTrace();
                    messages.add(new ImportMessage(ImportMessageType.ERROR, "Невозможно сохранить информацию о товаре: " + e.getResult().getMsg()));
                }
            }

            messages.add(new ImportMessage(ImportMessageType.INFO, "Загружено успешно : " + okCount + " записей"));
            messages.add(new ImportMessage(ImportMessageType.INFO, "Не загружено      : " + String.valueOf(items.size() - okCount) + " записей"));
            messages.add(new ImportMessage(ImportMessageType.INFO, "ВСЕГО             : " + items.size() + " записей"));
        }
        catch (Exception e)
        {
            e.printStackTrace();
            messages.add(new ImportMessage(ImportMessageType.ERROR, "Системная ошибка: " + e.toString()));
        }
    }

    /**
     * Стройка прейскуранта в Excel
     */
    public class ExcelPricelistItem
    {
        /**
         * Номер строки (в файле Excel)
         */
        private final int line;

        /**
         * Код товара
         */
        private String code = "";

        /**
         * Наименование товара
         */
        private String name = "";

        /**
         * Наименование группы товаров
         */
        private String goodTypeName = "";

        /**
         * M&H
         */
        private String mh = "";

        /**
         * о.е.
         */
        private String oe = "";

        /**
         * Применимость
         */
        private String pr = "";

        /**
         * Группова тара
         */
        private String packageCount;

        /**
         * Минимальная цена
         */
        private float minPrice;

        /**
         * Цены на позицию прейскуранта
         */
        private final Map<Long /*clientGroupId*/, Float /*price*/> prices = new HashMap<Long, Float>();

        /**
         * Идентификатор группы товаров
         */
        private long goodTypeId = -1;

        /**
         * Констркутор
         * @param line номер строки (в файле Excel)
         */
        public ExcelPricelistItem(int line)
        {
            _assert(line > 0);

            this.line = line;
        }

        public String normalizeCode(String code)
        {
            return code.replaceAll("  ", " ")
                        .trim()
                        .toUpperCase()
                        .replaceAll("А", "A")
                        .replaceAll("В", "B")
                        .replaceAll("Е", "E")
                        .replaceAll("К", "K")
                        .replaceAll("М", "M")
                        .replaceAll("Н", "H")
                        .replaceAll("О", "O")
                        .replaceAll("Р", "P")
                        .replaceAll("С", "C")
                        .replaceAll("Т", "T")
                        .replaceAll("У", "Y")
                        .replaceAll("Х", "X");
        }

        /**
         * Установить значение из указанной ячейки
         * @param cell номер колонки
         * @param value значение
         * @param columns карта соответствий мноеров колонок и клиентских групп (для колонок с ценами)
         */
        public void set(int cell, String value, Map<Integer /*cellNumber*/, Long /*clientGroupId*/> columns)
        {
            _assert(cell >= 0);
            _assertNotNull(value);
            _assertNotNull(columns);

            if (cell == getCODE_POS())       code = normalizeCode(value);
            if (cell == getGROUP_POS())      goodTypeName = s(value);
            if (cell == getNAME_POS())       name = s(value);
            if (cell == getMH_POS())         mh = s(value);
            if (cell == getOE_POS())         oe = s(value);
            if (cell == getPR_POS())         pr = s(value);
            if (cell == getMINPRICE_POS())   minPrice = new Float(Float.parseFloat(value)).intValue();
            if (cell == getPC_POS())
            {
                if (StringUtils.isBlank(value)) packageCount = "1";
                else packageCount = value;
            }
            if (cell >= getGR_POS())
            {
                _assert(columns.containsKey(cell), "Не для всех колонок с ценами проставлены соответствующие клиентские группы. Например для колонки " + cell);
                long clientGroupId = columns.get(cell);
                prices.put(clientGroupId, (float) new Float(Float.parseFloat(value)).intValue());
            }
        }

        /**
         * Ошибка в данной строчке прейскуранта: ячейка не заполнена
         */
        private final int ERROR_EMPTY = 0;

        /**
         * Ошибка в данной строчке прейскуранта: ячейка заполнена неверно
         */
        private final int ERROR_WRONG = 1;

        /**
         * Возвращает текст сообщения об ошибке используя переданные параметры.
         * @param error тип ошибки (ERROR_EMPTY, ERROR_WRONG)
         * @param name наименование колонки с ошибкой
         * @param line номер строки
         * @return текст сообщения об ошибке
         */
        private String getErrorText(int error, String name, int line)
        {
            new Throwable().printStackTrace();
            if (error == ERROR_EMPTY)   return "Не заполнено поле \"" + name + "\" (строка " + String.valueOf(line + 1) + ")";
            if (error == ERROR_WRONG)   return "Неверно заполнено поле \"" + name + "\" (строка " + String.valueOf(line + 1) + ")";
            return "";
        }

        /**
         * Функция проверяет данную строку прейскуранта и возвращает перечень сообщений
         *
         * @see ru.niir.goodfil.ui.ImportPricelistBean.ImportMessage сообщение для пользователя, возникающее при загрузке прейскуранта
         */
        public List<ImportMessage> validate()
        {
            List<ImportMessage> messages = new ArrayList<ImportMessage>();
            if (isEmpty(code))            messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_EMPTY, getColumnName(getCODE_POS()), line)));
            if (isEmpty(name))            messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_EMPTY, getColumnName(getNAME_POS()), line)));
            if (isEmpty(goodTypeName))    messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_EMPTY, getColumnName(getGROUP_POS()), line)));
            if (isEmpty(packageCount))        messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_WRONG, getColumnName(getPC_POS()), line)));
            if (minPrice <= 0)            messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_WRONG, getColumnName(getMINPRICE_POS()), line)));

            for (float f : prices.values())
                if (f < 0) messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_WRONG, "Цена", line)));


            long goodTypeId = -1;
            for (GoodType goodType : getGoodTypes())
                if (goodType.getName().trim().equals(goodTypeName.trim()))
                    goodTypeId = goodType.getId();

            if (goodTypeId == -1)           messages.add(new ImportMessage(ImportMessageType.ERROR, getErrorText(ERROR_WRONG, getColumnName(getGROUP_POS()), line)));
            else                            setGoodTypeId(goodTypeId);
            
            return messages;
        }

        /**
         * Номер строки (в файле Excel)
         */
        public int getLine()
        {
            return line;
        }

        /**
         * Код товара
         */
        public String getCode()
        {
            return code;
        }

        /**
         * Наименование товара
         */
        public String getName()
        {
            return name;
        }

        /**
         * Наименование группы товара
         */
        public String getGoodTypeName()
        {
            return goodTypeName;
        }

        /**
         * M&H
         */
        public String getMh()
        {
            return mh;
        }

        /**
         * о.е.
         */
        public String getOe()
        {
            return oe;
        }

        /**
         * Применимость
         */
        public String getPr()
        {
            return pr;
        }

        /**
         * Значение колонки "Группова тара"
         */
        public String getPackageCount()
        {
            return packageCount;
        }

        /**
         * Минимальная цена (колонка "Торговый дом")
         */
        public float getMinPrice()
        {
            return minPrice;
        }

        /**
         * Идентификатор группы товаров
         */
        public long getGoodTypeId()
        {
            return goodTypeId;
        }

        /**
         * Идентификатор группы товаров
         */
        public void setGoodTypeId(long goodTypeId)
        {
            _assert(goodTypeId > 0);
            this.goodTypeId = goodTypeId;
        }

        /**
         * Колонки с ценами
         */
        public Map<Long /*clientGroupId*/, Float /*price*/> getPrices()
        {
            return prices;
        }

        @Override
        public String toString()
        {
            return "ExcelPricelistItem{" +
                   "goodTypeId=" + goodTypeId +
                   ", prices=" + prices +
                   ", minPrice=" + minPrice +
                   ", packageCount=" + packageCount +
                   ", pr='" + pr + '\'' +
                   ", oe='" + oe + '\'' +
                   ", mh='" + mh + '\'' +
                   ", goodTypeName='" + goodTypeName + '\'' +
                   ", name='" + name + '\'' +
                   ", code='" + code + '\'' +
                   ", line=" + line +
                   '}';
        }
    }

    /**
     * Функция сохранения файла в общем каталоге на сервере
     * @param inFile файл
     * @param ext расширение
     * @return GUID, в который был сохранен файл
     */
    public synchronized UUID saveFile(File inFile, String ext)
    {
        assert inFile != null;
        assert ext != null;

        UUID newId = UUID.randomUUID();
        String newFilePath = getSHARED_CATALOG() + File.separator + newId + "." + ext;
        logger.debug("New file path: " + newFilePath);

        try
        {
            File outFile = new File(newFilePath);
            copyFile(inFile, outFile);
            return newId;
        }
        catch (IOException e)
        {
            logger.error(e);
            e.printStackTrace();

            return null;
        }
    }

    private static void copyFile(File in, File out) throws IOException
    {
        FileInputStream fis = null;
        FileOutputStream fos = null;
        try
        {
            fis  = new FileInputStream(in);
            fos = new FileOutputStream(out);

            byte[] buf = new byte[1024];
            int i;
            while ((i = fis.read(buf)) != -1) fos.write(buf, 0, i);
        }
        finally
        {
            if (fis != null) fis.close();
            if (fos != null) fos.close();
        }
    }

    private int uploadsAvailable = 1;

    public int getUploadsAvailable()
    {
        return uploadsAvailable;
    }

    public void setUploadsAvailable(int uploadsAvailable)
    {
        this.uploadsAvailable = uploadsAvailable;
    }

    public String clearUploadData()
    {
        setUploadsAvailable(1);
        return null;
    }
}
